﻿/*
 * Originally written by Vladimir Gondarev 
 * Copyright (c) Vladimir Gondarev  All rights reserved.
 * see license.txt
 */
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;

namespace Relusion.Microsoft.Translator
{
    public sealed class MicrosoftTranslateService
    {
        private const string TranslatorAccessURI = "https://datamarket.accesscontrol.windows.net/v2/OAuth2-13";
        private readonly string _clientId;
        private readonly string _clientSecret;
        private OAuthToken _currentAccessToken;

        /// <summary>
        /// Creates an instance of MicrosoftTranslateService
        /// </summary>
        /// <param name="clientId">ClientId for the Azure datamarket</param>
        /// <param name="clientSecret">Account key for the Azure datamarket</param>
        public MicrosoftTranslateService(string clientId, string clientSecret)
        {
            _clientId = clientId;
            _clientSecret = clientSecret;
        }

        /// <summary>
        /// Returns designated enum value for a language code.
        /// </summary>
        /// <param name="languageCode">language code, such as: "en","de","ru" etc.</param>
        /// <returns>enum value</returns>
        public BingLanguage ResolveLanguageByCode(string languageCode)
        {
            var enumType = typeof (BingLanguage);
            var languages = Enum.GetValues(enumType);
            
            foreach (var language in languages)
            {
                var attr = enumType                
                    .GetRuntimeField(language.ToString())
                    .GetCustomAttributes(typeof (BingApiLanguageCodeAttribute), false)
                    .SingleOrDefault() as BingApiLanguageCodeAttribute;

                if (attr != null && attr.LanguageCodeStr == languageCode)
                    return (BingLanguage)language;
            }

            throw new Exception("No languages with the language code: " + languageCode+" were found");
        }

        /// <summary>
        /// Translates string from one language to another
        /// </summary>
        /// <param name="orgString">text to be translated</param>
        /// <param name="orgLang">Source language</param>
        /// <param name="targetLang">Target language</param>
        /// <returns>translated text</returns>
        public async Task<string> TranslateString(string orgString, BingLanguage orgLang, BingLanguage targetLang)
        {
            if(targetLang == BingLanguage.AutoDetect)
                throw new ArgumentException("Target language should be explicitly set");

            await ValidateToken();
            string strTextToTranslate = orgString;
            string fromLangStr = orgLang != BingLanguage.AutoDetect ? ResolveAPILangCode(orgLang) : null;
            string toLangStr = ResolveAPILangCode(targetLang);
            string uri = "http://api.microsofttranslator.com/v2/Http.svc/Translate?text=" +
                         Uri.EscapeUriString(strTextToTranslate) + "&to=" + toLangStr;

            if (fromLangStr != null)
                uri +="&from=" + fromLangStr;

            WebRequest translationWebRequest = WebRequest.Create(uri);
            // The authorization header needs to be "Bearer" + " " + the access token
            string headerValue = "Bearer " + _currentAccessToken.AccessToken;
            translationWebRequest.Headers["Authorization"] = headerValue;

            WebResponse responce =
                await
                    Task.Factory.FromAsync<WebResponse>(translationWebRequest.BeginGetResponse,
                        translationWebRequest.EndGetResponse, translationWebRequest);
            Stream responseStream = responce.GetResponseStream();
            XDocument xTranslation = XDocument.Load(responseStream);
            string strTest = xTranslation.Root.FirstNode.ToString();
            return strTest;
        }

        private string ResolveAPILangCode(BingLanguage lang)
        {
            var attribute = lang.GetType()
                .GetRuntimeField(lang.ToString())
                .GetCustomAttributes(typeof (BingApiLanguageCodeAttribute), false)
                .SingleOrDefault() as BingApiLanguageCodeAttribute;
            return attribute == null ? lang.ToString() : attribute.LanguageCodeStr;
        }

        private async Task ValidateToken()
        {
            if (_currentAccessToken == null || TokenExpired(_currentAccessToken))
            {
                _currentAccessToken = await RequestToken();
            }
        }

        private bool TokenExpired(OAuthToken currentAccessToken)
        {
            string expireOnStrValue = ExtractExpireOnValue(currentAccessToken.AccessToken);
            DateTime expireDateTime = GetExpiryTime(expireOnStrValue);
            return DateTime.Now.ToUniversalTime() > expireDateTime;
        }

        private string ExtractExpireOnValue(string accessToken)
        {
            foreach (string nameValue in accessToken.Split('&'))
            {
                string[] keyValueArray = nameValue.Split('=');

                if ((keyValueArray.Length != 2)
                    && !String.IsNullOrEmpty(keyValueArray[0]))
                {
                    throw new Exception("The received token is not correctly formed");
                }
                if (String.IsNullOrEmpty(keyValueArray[1]))
                {
                    // ignore parameter with empty values
                    continue;
                }

                string key = Uri.UnescapeDataString(keyValueArray[0].Trim());

                if (key == "ExpiresOn")
                    return Uri.EscapeDataString(keyValueArray[1].Trim().Trim('"'));
            }

            return null;
        }        

        private async Task<OAuthToken> RequestToken()
        {
            WebRequest req = WebRequest.Create(TranslatorAccessURI);
            req.Method = "POST";
            req.ContentType = "application/x-www-form-urlencoded";

            Task<Stream> requestStreamTask = Task.Factory.FromAsync<Stream>(req.BeginGetRequestStream,
                req.EndGetRequestStream, req);
            
            using (Stream reqStream = await requestStreamTask)
            {
                String strRequestDetails =
                    string.Format(
                        "grant_type=client_credentials&client_id={0}&client_secret={1}&scope=http://api.microsofttranslator.com",
                        Uri.EscapeDataString(_clientId), Uri.EscapeDataString(_clientSecret));

                byte[] data = Encoding.UTF8.GetBytes(strRequestDetails);
                await reqStream.WriteAsync(data, 0, data.Length);             
            }
            try
            {
                WebResponse responce =
                    await Task.Factory.FromAsync<WebResponse>(req.BeginGetResponse, req.EndGetResponse, req);
                var serializer = new
                    DataContractJsonSerializer(typeof (OAuthToken));
                return (OAuthToken) serializer.ReadObject(responce.GetResponseStream());
            }
            catch (WebException ex)
            {
                var serializer = new DataContractJsonSerializer(typeof (ServerSideError));
                var errorObject = (ServerSideError) serializer.ReadObject(ex.Response.GetResponseStream());
                throw new Exception(errorObject.ErrorDescription);
            }
        }

        static DateTime GetExpiryTime(string expiresOnValue)
        {
            int expiresOnUnixTicks = int.Parse(expiresOnValue);
            var epochStart = new DateTime(1970, 01, 01, 0, 0, 0, 0, DateTimeKind.Utc);
            return epochStart.AddSeconds(expiresOnUnixTicks);
        }
    }
}